// Copyright 2012 Jay Young. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview
 *
 * Provides a class that handles setting up the node visitor chain and
 * running nodes through it.
 */

var fs = require('fs'),
    strings = require('ringo/utils/strings'),
    DocMap = require('rdoc/docmap').DocMap,
    DocInfo = require('rdoc/docinfo').DocInfo,
    logger = require('ringo/logging').getLogger('Documentor');



/**
 * A class that handles setting up the node visitor chain and running
 * nodes through it.
 *
 * @param {JSTypeRegistry} typeReg The type registry for this file.
 * @param {ConfigParser} config The config object.  Used to map JS files to
 *    output files.
 *
 * @constructor
 */
var Documentor = exports.Documentor = function(typeReg, config) {
  /**
   * The front of the visitor queue.
   *
   * @type {Documentor.Visitor}
   * @private
   */
  this.headVisitor_ = null;

  /**
   * The end of the visitor queue.  New visitors are added to this side.
   *
   * @type {Documentor.Visitor}
   * @private
   */
  this.tailVisitor_ = null;

  /**
   * The type registry for this file.
   * @param {JSTypeRegistry}
   * @private
   */
  this.typeRegistry_ = typeReg;

  /**
   * The config object for this run.
   *
   * @type {ConfigParser}
   * @private
   */
  this.config_ = config;
};


/**
 * Set up the visitor chain.
 * @return {Documentor} Returns this for chaining.
 */
Documentor.prototype.buildVisitorChain = function() {
  var {FileOverviewV} = require('rdoc/visitors/fileoverview'),
      {GoogScopeV} = require('rdoc/visitors/googscope'),
      {ConstructorV} = require('rdoc/visitors/constructor'),
      {MethodV} = require('rdoc/visitors/method'),
      {EnumV} = require('rdoc/visitors/enum'),
      {PropertyV} = require('rdoc/visitors/property'),
      {TypedefV} = require('rdoc/visitors/typedef'),
      {verbose} = this.config_;

  return this.addVisitor(new FileOverviewV())
             .addVisitor(new GoogScopeV(verbose))
             .addVisitor(new ConstructorV())
             .addVisitor(new MethodV())
             .addVisitor(new EnumV())
             .addVisitor(new PropertyV())
             .addVisitor(new TypedefV());
};


/**
 * Adds a visitor to the documenter.
 *
 * @param {Documentor.Visitor} visitor The visitor to add.
 * @return {Documentor} Returns this for chaining.
 */
Documentor.prototype.addVisitor = function(visitor) {
  if (this.headVisitor_ === null) {
    this.headVisitor_ = this.tailVisitor_ = visitor;
  }
  else {
    this.tailVisitor_.setNext(visitor);
    this.tailVisitor_ = visitor;
  }

  return this;
};


/**
 * Document an array of SCRIPT nodes.
 *
 * @param {Array.<Node>} scripts The array of script nodes to document.
 * @param {boolean=} verbose Print lots of status updates as we go.  Defaults to
 *    false.
 * @return {DocMap} A documentation map containing all of the types and a list
 *    of the generated documentation files.
 */
Documentor.prototype.documentScripts = function(scripts, verbose) {
  var docMap = new DocMap(),
      config = this.config_,
      Node = com.google.javascript.rhino.Node;

  var force = config.force,
      totalElapsed = 0;

  logger.info('Analyzing AST and generating intermediate doc files.');
  for (var i = 0, len = scripts.length; i < len; ++i) {
    var script = scripts[i],
        filename = script.getProp(Node.STATIC_SOURCE_FILE).toString();

    var start, elapsed;
    if (verbose) {
      start = Date.now();
    }

    var {docFile, rev} = this.getOutputFile(filename);

    // If the doc file exists already, compare its lastModified time to the
    // time the source file was last modified.  If the doc file is newer,
    // skip processing the source file. Instead, just read the doc file
    // from disk and add it to the doc map.

    var modified = force;
    if (! modified) {
      var docced = fs.isFile(docFile) ? fs.lastModified(docFile).valueOf() : 0;
      modified = docced < fs.lastModified(filename).valueOf();
    }

    var docInfo;
    if (modified) {
      docInfo = this.document(filename, rev, script);
      delete docInfo.typeRegistry_;

      fs.write(docFile, JSON.stringify(docInfo));
    }
    else {
      docInfo = DocInfo.fromFile(docFile);
      
      // If the library is now at a new revision, re-save the DocInfo object
      // with that new revision.
      if (docInfo.rev !== rev) {
        docInfo.rev = rev;
        fs.write(docFile, JSON.stringify(docInfo));
      }
    }

    scripts[i] = null;
    docMap.document(docInfo, docFile);

    if (verbose) {
      elapsed = Date.now() - start;

      totalElapsed += elapsed;
      var average = totalElapsed / (i + 1);

      var relative;
      if (elapsed > average) {
        relative = ' : Over  : Documenting ';
      }
      else if (elapsed < average) {
        relative = ' : Under : Documenting ';
      }
      else {
        relative = ' : Avg.  : Documenting ';
      }

      logger.info((i + 1) + relative + filename + ' took ' + elapsed +
                  'ms (avg: ' + Math.floor(average) + 'ms)');
    }

    if ((! verbose) && (i % 100 === 0) && i > 0) {
      logger.info(i + ' files written.');
    }
  }
  if (! verbose) {
    logger.info(i + ' files written.');
  }

  fs.write(this.config_.outputPath + 'docmap.json', JSON.stringify(docMap));
  return docMap;
};


/**
 * Begin traversal of the AST for this root node.
 *
 * @param {string} filename The node's source file name.
 * @param {?string} rev The revision of the file being documented.
 * @param {Node} node The root AST node.
 * @return {DocInfo} The populated documentation info object.
 */
Documentor.prototype.document = function(filename, rev, node) {
  var docInfo = new DocInfo(filename, rev, this.typeRegistry_);

  this.traverse(node, docInfo);
  return docInfo;
};


/**
 * Traverse the AST for the given node.
 *
 * @param {Node} node The AST node to traverse down.
 * @param {DocInfo} The documentation info object.
 */
Documentor.prototype.traverse = function(node, docInfo) {
  var jsdoc = node.getJSDocInfo();
  this.headVisitor_.visit(node, docInfo, jsdoc);

  if (node.hasChildren()) {
    var children = node.children();

    while (children.hasNext()) {
      this.traverse(children.next(), docInfo);
    }
  }
};


/**
 * @param {string} filename The filename of the JS file being documented.
 * @return {{docFile: string, rev: ?string}} The filename where the
 *    documentation for that file should be saved.
 */
Documentor.prototype.getOutputFile = function(filename) {
  return this.config_.getDocFile(filename);
};

/**
 * The interface that classes must implement in order to be usable as
 * node visitors.
 *
 * @interface
 */
Documentor.Visitor = function() {};


/**
 * A visitor's visit method must take the node and the doc info object for the
 * page being parsed.
 *
 * @param {Node} node The node to visit.
 * @parma {DocInfo} docInfo The page's documentation info object.
 * @param {JSDocInfo} jsdoc The jsdoc for the given node or null if no
 *    doc comments were attached.
 */
Documentor.Visitor.prototype.visit = function(node, docInfo, jsdoc) {};


/**
 * Visitors are chained together so that nodes go in one end and come out the
 * other.  Each visitor is responsible for passing nodes off to the next when
 * they are done with them.
 *
 * @param {Visitor} nextVisitor The next visitor in the chain.
 */
Documentor.Visitor.prototype.setNext = function(nextVisitor) {};
